Repository navigation
fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant - #30601
Conversation
Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
…ases Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
…aleAmong Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
…plate row The fold pin asserted a zh-MO recipient of a zh-TW + zh-HK + zh-CN bundle got the Simplified row; the table-language step now answers zh-HK. The variant expansion pin moves to a code the table does not know (es-CO). Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d705b033206cd021842ba64830ad066dad9e2eaf && git checkout d705b033206cd021842ba64830ad066dad9e2eaf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ffeda137b8412b11c909a6d00357f46e0cd555d2 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5 && git checkout -B drift-repro ffeda137b8412b11c909a6d00357f46e0cd555d2 && git merge --no-ff 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5
node scripts/docs-audit/affected-docs.mjs --json ffeda137b8412b11c909a6d00357f46e0cd555d2
|
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract-review-claim: session_01L6bhgTqqeHSwZYJmApbDCa · head 9f692b7 · 2026-11-05T17:42Z Full review at Generated by Claude Code |
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract reviewServed-tier: Inputs read: card #30538 (body, triage 6661339702, re-route 6661724543, claim 6663592658, amend 6663692830, dev report 6664917386); PR #30601 body, file list (13 files, +223/−53) and the net diff of Gate verdicts, read at 2026-11-05T18:06Z (the last run completed 18:06Z): 69 check-runs on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentJudged on the live tree against the scenario the card accepts: a
Must-fix: none. Should-fix:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…rm table step Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
…escribe Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
resolveOfferedLocale (from main) now delegates to resolveLocaleAmong, so the offered-locale matcher and the resolvers run one ladder. Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract review — deltaServed-tier: Delta reviewed: the three commits since Gate verdicts on S1 and S2 — true at this head
The merge resolution in
|
Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa Contract review — deltaServed-tier: Delta reviewed: the three commits since Gate verdicts on The new changeset paragraph — true, and it matches the finding
Findings
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa 维护者速读
席位意见:契约复审三轮都 PASS。第一轮的 S1、S2 在本 PR 里修了,S3–S6 另立卡或挂到对应卡上。合入 main 时和 #30589 冲突,处理成全仓只剩一套语言匹配规则;第二轮复审实测了这对 #30589 调用方的唯一影响(一个并列取舍),并要求在 changeset 里写明,第三轮确认已写。 席位核对 — ACCEPT · domain:spec#3 · head
|
… restate four steps PR #30601 put both server locale resolvers on resolveLocaleAmong (exact, case-insensitive, platform locale table language, base language, first registered variant). Comments in objectql, service-automation, plugin-auth and plugin-email, the translations docs page and the email-template-render checklist item still listed the old four steps. They now name the resolver and its five steps, and say the table step only adds answers for the table's aliases. Comment, docs and checklist text only; no behaviour change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cytQjG9X1CrCgytUM5
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa
Requested by Sales · project thread
Fixes #30538
Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: follow-up #30603 — skills/objectstack-i18n/SKILL.md (its in-memory fallback paragraph restates the four-step ladder without the table step;
skills/**is a Tier H surface, so it is not edited here)Branch:
claude/project-thread-mros1u(the card thread's platform-assigned branch, restated in the claim 6663692830; not theclaude/issue-30538-…convention).维护者速读
resolveBundleLocale、core 的resolveLocale)改为同一份实现,先按平台语言表的读别名找同一语言,再退到基础语言和第一个变体。packages/spec/src/system/i18n-resolver.ts的resolveLocaleAmong第 3 步;packages/core/src/fallbacks/memory-i18n.ts;packages/core/src/fallbacks/fallbacks.test.ts的共享用例表。pnpm --filter @objectstack/core exec vitest run src/fallbacks/fallbacks.test.ts,预期 107 例全绿,其中共享用例表对两个解析器各跑一遍,另有一例断言resolveOfferedLocale与resolveLocale答案相同。What changes
@objectstack/spec/systemgainsresolveLocaleAmong(requested, available), the platform's one locale-matching ladder over a list: exact, case-insensitive, the same platform locale table language, base language, first registered variant.platformLocaleTagOf(the table's read aliases, the one alias list; no second list is added) and picks the key spelled as that table tag, else the first key the table maps to it. Sozh-TW,zh-HK,zh-MOreach a registeredzh-Hant, andzh,zh-CN,zh-SG,zh-Hansreachzh-CN, in either registration order.resolveBundleLocalekeeps its exact fast path and then runsresolveLocaleAmongover the bundle's keys. Core'sresolveLocaleisresolveLocaleAmong(core already imports@objectstack/spec/system, so there is no new import edge and no cycle). The two answers cannot drift.resolveBundleLocale, core'sresolveLocale,platformLocaleTagOf(its paragraph stated thatresolveBundleLocale({ 'zh-Hant': …, 'zh-Hans': … }, 'zh')answerszh-Hant, which this PR makes false), the email ladder of record (SendTemplateInput.localeinpackages/spec/src/contracts/email-service.ts), its copy incontent/docs/automation/email-templates.mdx, thestack.zod.tsemail floor docblock, and plugin-email's ladder comment inemail-service.ts.api-surface/system.jsonandexport-origins/system.jsonregenerated for the new export.@objectstack/specminor,@objectstack/coreminor.Tests
packages/spec/src/system/i18n-resolver.test.ts, new top-level describeresolveBundleLocale — the platform table language step: zh-TW / zh-HK / zh-MO / zh-tw / zh-Hant →zh-Hantand zh-CN / zh / zh-SG / zh-Hans / zh-cn →zh-CNin both registration orders; table-tag spelling preferred; a barezhkey counts as Simplified;ja→ja-JP,es→es-ES,en→en-USagainst alias-spelled bundles; unknown codes (es-MX,en-GB,pt-BRoverpt-PT,fr, empty bundle) fall back as before.packages/plugins/plugin-email/src/template-locale-fold.test.ts, a moved pin: it asserted that azh-MOrecipient of azh-TW+zh-HK+zh-CNbundle got the Simplifiedzh-CNrow, which is this card's defect. It now asserts the Traditionalzh-HKrow in both list orders (pluszh-TW→zh-Hantandzh→zh-CNoveren-US+zh-CN+zh-Hant), and the variant-expansion pin moves to a code the table does not know (es-CO→es-AR, both list orders).packages/core/src/fallbacks/fallbacks.test.ts, new top-level describeresolveLocale and resolveBundleLocale — one shared case list: one 20-row table run against both resolvers, plus the in-memory i18n service serving azh-HKreader thezh-Hantbundle and azhreader thezh-CNone. The existingresolveLocalepins (includingzh-TWover['en', 'zh']→zh) are unchanged and green.Acceptance notes
Clause-②: yes (widening): the shared implementation needs a list-shaped entry point, so@objectstack/spec/systempublishes one new function (no export removed or narrowed).Metadata-protocol: semantic: a translation bundle key or email template row an author writes aszh-Hantnow serveszh-TW/zh-HK/zh-MOreaders it did not, a bundle registeringzh-Hantbeforezh-CNnow serveszhreaders the Simplified key, and the publishedresolveBundleLocaledocblock promised the old four-step order.check-changeset-no-majorreading in Verification.2ffdb649,resolveBundleLocale({ zh: {}, 'zh-Hant': {} }, 'zh-TW')answered'zh': the base-language step hands a Traditional reader a barezhkey, which the table reads as Simplified. The table step therefore runs before base language (step 3 of 5). It only adds earlier answers, so no request that matched before stops matching.platform-locales.ts(theplatformLocaleTagOfparagraph; the table and aliases are untouched),contracts/email-service.ts,stack.zod.ts,content/docs/automation/email-templates.mdx, plugin-email'semail-service.tscomment. Each restated the old order in a way this change makes false for a bundle that holds both Chinese scripts. One test: plugin-email'stemplate-locale-fold.test.ts, whose pin asserted the defect (see Tests).docs/qa/platform-checklist/areas/integration-system.jsonacceptance[3] copies the email ladder (a revisioned QA file; the showcase bundle carrieszh-CNonly, so its verify line is unchanged);packages/plugins/plugin-auth/src/idp-locale-claim.ts'sresolveLocalesummary (plugin-auth's copy tables are keyeden/zh-CN/ja-JP/es-ES, whose answers do not move). Carrier: the spec 席 3 seat.pickRenderableLocale(ui/packages/app-shell/src/hooks/useUserLocale.ts) still runs the old four steps; it is i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511's surface and not touched here.main(fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589). The merge in51ace3famet fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's newresolveOfferedLocaleinpackages/core/src/fallbacks/memory-i18n.ts, a second matching ladder. The resolution makes itreturn resolveLocaleAmong(...), so the repo keeps one ladder. fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's pin infallbacks.test.tsthatresolveLocaleanswerszh-Hansforzh-TWasserted this card's defect; it now expectszh-Hant, and a new case asserts thatresolveOfferedLocaleandresolveLocalegive the same answer. The contract review's delta (6667101706) measured the one answer that moves for fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's callers, which are Setup's default-language options (service-settings/src/localization-locale-options.ts:125) and the IdP locale claim (plugin-auth/src/idp-locale-claim.ts:148). It is a tie-break inside the table step: when a list offers two codes of one table language, the code spelled as the table tag wins over the first one offered (zhover['zh-CN', 'zh-Hans']answerszh-Hans, wheremainansweredzh-CN). Admission never changes, no shipped bundle registers such a pair, and the@objectstack/corechangeset carries the sentence. Review findings S1 (three spots inemail-template.zod.ts, its reference page regenerated) and S2 (thebuiltinSystemFieldLabelandvalidation-message.tsdocblocks) landed in16eafb26.resolveBundleLocale's exact fast path readsbundle[requested], so over a plain-object bundle a request spelled like anObject.prototypemember (constructor) answers that name. Unchanged by this PR and not measured at a public door;resolveLocaleAmongusesincludesand does not do this.Verification
Round 2, at head
51ace3fa(S1 and S2, plusorigin/mainmerged in), after a full build (119/119):success, 8skipped, 0 failed.@objectstack/spec816 files / 25719 tests + 1 todo;@objectstack/core135 / 3075 (fallbacks.test.ts107);@objectstack/service-settings69 / 1178;@objectstack/plugin-email48 / 801; plugin-auth's locale tests 6 / 202.check:generatedexit 0.dispatch-gates --commands: 109 derived, all exit 0;--ran: 109 run, 0 NOT-MEASURED, 0 UNRUN.Round 1 readings below are at head
9f692b76unless named.Repro before the change (origin/main 2ffdb64)
Package suites and typecheck
pnpm --filter @objectstack/spec test: Test Files 815 passed (815), Tests 25668 passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/core test: Test Files 135 passed (135), Tests 3070 passed.pnpm --filter @objectstack/core typecheck(tsc, examples, test-typecheck): exit 0.pnpm --filter @objectstack/plugin-email test: Test Files 48 passed (48), Tests 796 passed.typecheck: exit 0.check:generatednamedapi-surface/andexport-origins/stale; both regenerated and committed.--no-inline-configon the 9 changed source and test files: exit 0.check-nul-bytes: exit 0.Consumers reached (targeted locale suites, after a full build 119/119)
@objectstack/runtime(the/i18nREST door'sresolveLocale, hook refusal catalogue, flow run locale): 8 files, 126 passed.@objectstack/plugin-auth(IdP locale claim, MFA and IP-range refusal copy): 6 files, 201 passed.@objectstack/service-settings(currency lock, locale options copy): 2 files, 38 passed.@objectstack/objectql(validation message negotiation): 4 files, 44 passed.@objectstack/plugin-approvals: 2 files, 25 passed.@objectstack/plugin-audit: 2 files, 19 passed.@objectstack/plugin-email(template locale fold): 4 files, 98 passed after the moved pin above (1 failed before it: the defect pin).@objectstack/service-automation: 3 files, 43 passed.@objectstack/service-messaging: 2 files, 30 passed.@objectstack/service-i18n: 2 files, 52 passed.Ablation (remove the table-language step)
scripts/ablation-replace.mjs --deleteon the step-3 block ofresolveLocaleAmong(anchor 1 → 0, blob 97078210 → 47872494), spec rebuilt,ablation-dist-preflight --absent: marker absent from all 592 built files.i18n-resolver.test.ts: 10 failed / 482 passed (every zh-TW / zh-HK / zh-MO / zh / zh-SG / zh-Hans case of the new describe). corefallbacks.test.ts: 15 failed / 87 passed (the zh rows of the shared case list for both resolvers, and the in-memory service case).git diff HEADempty, tree clean; rebuilt, preflight marker present in 2 built files; spec 492 passed, core 102 passed.Derived gates (dispatch-gates --commands at 9f692b7)
exit 0.--ranreconciliation: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).check-changeset-no-major --base origin/main: nomajorbump; the level axis is PR-scoped and reads this body in CI.check-adr-0087-registration --base origin/main: no declared-breaking changeset.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb